<?php

require_once 'OpenID.php';
require_once 'OpenID/Store.php';
require_once 'OpenID/Discover.php';
require_once 'OpenID/Association/Request.php';
require_once 'OpenID/Assertion.php';
require_once 'OpenID/Assertion/Result.php';
require_once 'OpenID/Auth/Request.php';

class OpenID_RelyingParty extends OpenID
{
    protected $normalizedID = null;
    protected $returnTo = null;
    protected $realm = null;
    protected $useAssociations = true;
    protected $clockSkew = null;

    public function __construct($identifier,
                                $returnTo,
                                $realm)
    {
        $this->normalizedID       = OpenID::normalizeIdentifier($identifier);
        $this->returnTo           = $returnTo;
        $this->realm              = $realm;
    }

    public function enableAssociations()
    {
        $this->useAssociations = true;
    }

    public function disableAssociations()
    {
        $this->useAssociations = false;
    }

    public function setClockSkew($skew)
    {
        if (!is_numeric($skew)) {
            throw new OpenID_Exception(
                'Invalid clock skew'
            );
        }
        $this->clockSkew = $skew;
    }

    public function prepare()
    {
        // Discover
        $discover        = $this->getDiscover();
        $serviceEndpoint = $discover->services[0];

        // Associate
        $assocHandle = null;
        if ($this->useAssociations) {
            $opEndpointURL = array_shift($serviceEndpoint->getURIs());
            $assoc         = $this->getAssociation($opEndpointURL,
                                                   $serviceEndpoint->getVersion());

            if ($assoc instanceof OpenID_Association) {
                $assocHandle = $assoc->assocHandle;
            }
        }

        // Return OpenID_Auth_Request object
        return new OpenID_Auth_Request($discover,
                                       $this->returnTo,
                                       $this->realm,
                                       $assocHandle);
    }

    /**
     * Verifies an assertion response from the OP.  If the openid.mode is error, an
     * exception is thrown.
     * 
     * @param OpenID_Message $message The Assertion response from the OP
     * 
     * @throws OpenID_Exception on error
     * @return OpenID_Assertion_Response
     */
    public function verify(OpenID_Message $message)
    {
        $mode     = $message->get('openid.mode');
        $result = new OpenID_Assertion_Result;

        switch ($mode) {
            case OpenID::MODE_ID_RES:
                break;
            case OpenID::MODE_CANCEL:
            case OpenID::MODE_SETUP_NEEDED:
                $result->setAssertionMethod($mode);
                return $result;
            case OpenID::MODE_ERROR:
                throw new OpenID_Exception($message->get('openid.error'));
            default:
                throw new OpenID_Exception('Unknown mode: ' . $mode);
        }

        $discover        = $this->getDiscover();
        $serviceEndpoint = $discover->services[0];
        $opEndpointURL   = array_shift($serviceEndpoint->getURIs());
        $assertion       = new OpenID_Assertion($message,
                                                $this->returnTo,
                                                $this->clockSkew);

        // Check via associations
        if ($this->useAssociations) {
            // Invalidate handle?
            if ($message->get('openid.invalidate_handle') !== null) {
                // Delete and fall back to check_authenticate
                $this->getStore()->deleteAssociation($opEndpointURL);
            } else {
                $result->setAssertionMethod(OpenID::MODE_ASSOCIATE);
                $assoc = $this->getStore()->getAssociation($opEndpointURL);
                if ($assoc instanceof OpenID_Association &&
                    $assoc->checkMessageSignature($message)) {
                    $result->setAssertionResult(true);
                }
                return $result;
            }
        }

        // Check via check_authenticate
        $result->setAssertionMethod(OpenID::MODE_CHECK_AUTHENTICATION);
        $result->setCheckAuthResponse($assertion->checkAuthentication());
        if ($result->getCheckAuthResponse()->get('is_valid') == 'true') {
            $result->setAssertionResult(true);
        }
        return $result;
    }

    protected function getDiscover()
    {
        $discover = OpenID_Discover::getDiscover($this->normalizedID,
                                                 $this->getStore());
        if ($discover === false) {
            throw new OpenID_Exception('Unable to discover OP Endpoint URL');
        }

        return $discover;
    }

    protected function getAssociation($opEndpointURL, $version)
    {
        $assocCache = $this->getStore()->getAssociation($opEndpointURL);

        if ($assocCache instanceof OpenID_Association) {
            return $assocCache;
        }

        $assoc  = new OpenID_Association_Request($opEndpointURL, $version);
        $result = $assoc->associate();

        if (!$result instanceof OpenID_Association) {
            return false;
        }

        self::getStore()->setAssociation($result);

        return $result;
    }
}
?>
